﻿// This File is part of Innovation Foundry's "QualityHelper Project"
//
// Copyright  2009 Innovation Foundry Pty Ltd
// All rights reserved
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the
// Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA  02110-1301 USA
// or alternatively it may be downloaded from <http://www.gnu.org/licenses/>.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace InnovationFoundry.QualityHelper.Silverlight.Controls.Zoom
{
    public class Zoomify : Canvas
    {

        private FrameworkElement diagram;
        private FrameworkElement diagramReflection; 
        private MouseWheelHelper mouseHelper;
        

        //posibly unused
        private TransformGroup tg;
        private ScaleTransform scale;
        private TranslateTransform trans;

        // calcuation values
        public bool isDragging = false;
        private Point clickPosition;
        private Point currentPosition;
        private Point lastMousePosition;
        private Point oldViewportCentre;
        private Point newViewportCentre;
        private Point viewportCentre;
        private Point newDiagramLocation;
        private Point oldDiagramLocation;
        private Point shiftAmount;
        private double newScale = 1f;
        private double oldScale = 1f;
        private double originalLeft = 0f;
        private double originalTop = 0f;
        private bool wasZoomedOut = false;

        // preset values
        private const double minDragPixels = 10f;
        private const double maxScale = 3f;
        private const double minScale = 0.4f;
        private const double incrementFactor = 0.10f;
        private TimeSpan animTime = TimeSpan.FromMilliseconds(500);
        private bool handled = false;

        public Zoomify() { }

        public FrameworkElement ContentToZoom
        {
            get
            {
                return diagram;
            }
            set
            {
                //if (diagram.Name == value.Name)
                //    return;
                diagram = value;
                if (diagram != null)
                {
                    if (!diagram.Name.Contains("ref") && !handled) {
                        handled = true;
                        this.diagram.MouseLeftButtonDown += new MouseButtonEventHandler(diagram_MouseLeftButtonDown);
                        this.diagram.MouseLeftButtonUp += new MouseButtonEventHandler(diagram_MouseLeftButtonUp);
                        this.diagram.MouseMove += new MouseEventHandler(diagram_MouseMove);
                        this.diagram.MouseLeave += new MouseEventHandler(diagram_MouseLeave);
                        this.diagram.MouseEnter += new MouseEventHandler(diagram_MouseEnter);
                        this.MouseLeftButtonUp += new MouseButtonEventHandler(Page_MouseLeftButtonUp);
                        mouseHelper = new MouseWheelHelper(this.Parent as Panel);
                        mouseHelper.Moved += new EventHandler<MouseWheelEventArgs>(mouseWheel_Moved);

                        //viewportCentre = new Point(diagram.RenderSize.Width / 2, diagram.RenderSize.Height / 2);
                        viewportCentre = new Point(diagram.Width / 2, diagram.Height / 2);
                        newViewportCentre = viewportCentre;
                        oldViewportCentre = viewportCentre;
                        newScale = 1;
                        oldScale = 1;
                        newDiagramLocation = new Point(0, 0);
                        
                    }                    
                }
            }
        }

        public FrameworkElement ReflectionToZoom
        {
            get
            {
                return diagramReflection;
            }
            set
            {
                diagramReflection = value;
            }
        }

            
        /// <summary>
        /// Stop dragging when mouse leaves diagram as we don't know mouse values when it leaves
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void diagram_MouseEnter(object sender, MouseEventArgs e) {
            if (isDragging)
                isDragging = false;
        }

        /// <summary>
        /// Stop dragging when mouse leaves diagram as we don't know mouse values when it leaves
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void diagram_MouseLeave(object sender, MouseEventArgs e) {
            isDragging = false;
            this.ReleaseMouseCapture();
        }

        /// <summary>
        /// Stop mouse capture and dragging if true
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Page_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            isDragging = false;
            this.ReleaseMouseCapture();
            this.Cursor = Cursors.Arrow;
            
        }
        private void move(bool invert) {
            tg = diagram.RenderTransform as TransformGroup;
            if (tg == null)
            {
                loadObjects(diagram);
                loadObjects(diagramReflection);
            }

            double moveX = (lastMousePosition.X - currentPosition.X) * newScale;
            double moveY = (lastMousePosition.Y - currentPosition.Y) * newScale;
            
            shiftAmount = new Point(moveX, moveY);

            //don't move if zoomed out past scale=1
            //if (newScale > 1) {
                newDiagramLocation = new Point(oldDiagramLocation.X + moveX, oldDiagramLocation.Y + moveY);
                performAnimation();
            //}
            
        }


        /// <summary>
        /// Move diagram if left mouse button held down
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void diagram_MouseMove(object sender, MouseEventArgs e)
        {
            
            if (diagram == null)
                return;
            lastMousePosition = e.GetPosition(this);
            if (isDragging)
                    move(true);
            currentPosition = e.GetPosition(this);
            lastMousePosition = currentPosition;
        }

        /// <summary>
        /// Stop dragging or if mouse has been click in single place, move diagram
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void diagram_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (diagram == null || e == null)
                return;
            isDragging = false;
            diagram.ReleaseMouseCapture();
            diagram.Cursor = Cursors.Arrow;
            lastMousePosition = e.GetPosition(this);
            double movedDistance  = distanceBetweenPoints(lastMousePosition,clickPosition);
        }


        /// <summary>
        /// Start dragging (dependent on location when button is not depressed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void diagram_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (diagram == null || e == null)
                return;
            isDragging = true;
            clickPosition = e.GetPosition(this);
            lastMousePosition = e.GetPosition(this);
            diagram.Cursor = Cursors.Hand;
            diagram.CaptureMouse();
        }

        /// <summary>
        /// Handle zoom in/out
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void mouseWheel_Moved(object sender, MouseWheelEventArgs e)
        {
            if (diagram == null)
                return;
            if (e.Delta > 0)
                ScaleUp(e);
            else if (e.Delta < 0)
                ScaleDown(e);
        }

        /// <summary>
        /// Reset the scale
        /// </summary>
        public void ResetScale()
        {
            if (diagram == null)
                return;

            ScaleTransform trans = new ScaleTransform();
            double diagramLeft = Double.Parse(diagram.GetValue(Canvas.LeftProperty).ToString());
            double diagramTop = Double.Parse(diagram.GetValue(Canvas.TopProperty).ToString());
            oldDiagramLocation.X = 0.0;
            oldDiagramLocation.Y = 0.0;
            newDiagramLocation.X = 0.0;
            newDiagramLocation.Y = 0.0;
            newScale = 1.0;
            oldScale = 1.0;
            if(diagramLeft != 0)
                diagram.SetValue(Canvas.LeftProperty, originalLeft);
            if(diagramTop != 0)
                diagram.SetValue(Canvas.TopProperty, originalTop);
            trans.ScaleX = 1;
            trans.ScaleY = 1;
            diagram.RenderTransform = trans;
            diagram_MouseLeftButtonUp(null, null);
        }



        /// <summary>
        /// Zoom out
        /// </summary>
        /// <param name="e"></param>
        public void ScaleDown(MouseWheelEventArgs e)
        {
            tg = diagram.RenderTransform as TransformGroup;
            if (tg == null)
            {
                loadObjects(diagram);
                loadObjects(diagramReflection);
            }

            
            if (newScale - incrementFactor > minScale)
            {
                newScale = oldScale - incrementFactor;
                if (newScale == 1)
                    newViewportCentre = viewportCentre;
                
                performAnimation();

            }
        }


        /// <summary>
        /// Zoom in
        /// </summary>
        /// <param name="e"></param>
        public void ScaleUp(MouseWheelEventArgs e)
        {
            tg = diagram.RenderTransform as TransformGroup;
            if (tg == null)
            {
                loadObjects(diagram);
                loadObjects(diagramReflection);
            }
            
            if (newScale < maxScale)
            {
                newScale = oldScale + incrementFactor;

                if (newScale > 1) {
                    newViewportCentre = new Point(lastMousePosition.X, lastMousePosition.Y);
                }

                performAnimation();
            }
        }


        /// <summary>
        /// Do the animation as per new values center and scale
        /// </summary>
        private void performAnimation() {
            double diagramLeft = Double.Parse(diagram.GetValue(Canvas.LeftProperty).ToString());
            double diagramTop = Double.Parse(diagram.GetValue(Canvas.TopProperty).ToString());
            Rect renderbounds;
            Rect diagrambounds;
            double diagramWidth = 0;
            double diagramHeight = 0;
            Point offsetsrc = new Point();
            
            
            offsetsrc = diagram.TransformToVisual(diagram.Parent as Canvas).Transform(new Point(0,0));
            offsetsrc.X += shiftAmount.X;
            offsetsrc.Y += shiftAmount.Y;
            diagramWidth = diagram.RenderSize.Width * newScale;
            diagramHeight = diagram.RenderSize.Height * newScale;

            renderbounds = new Rect(0, 0, diagram.RenderSize.Width, diagram.RenderSize.Height);
            diagrambounds = new Rect(offsetsrc.X, offsetsrc.Y, diagramWidth, diagramHeight);

            Point viewPortShift = new Point(viewportCentre.X - newViewportCentre.X, viewportCentre.Y - newViewportCentre.Y);

            double xdiff = -diagramWidth - offsetsrc.X + ((Canvas)diagram.Parent).Width;// -(viewPortShift.X * newScale);
            double ydiff = -diagramHeight - offsetsrc.Y + ((Canvas)diagram.Parent).Height;// -(viewPortShift.Y * newScale);

            if (newScale >= 1) {
                ///ALLOW MOVEMENT BACK TOWARDS CENTRE!!!!
                ///
                bool upMovement = (newDiagramLocation.Y - oldDiagramLocation.Y < 0);
                bool rightMovement = (newDiagramLocation.X - oldDiagramLocation.X > 0);
                // right clip
                if (xdiff > 0 && !rightMovement)
                {
                    newDiagramLocation.X = oldDiagramLocation.X;
                }
                // left clip
                if (offsetsrc.X > 0 && rightMovement)
                {
                    newDiagramLocation.X = oldDiagramLocation.X;
                }
                // top clip
                if (offsetsrc.Y > 0 && !upMovement)
                {
                    newDiagramLocation.Y = oldDiagramLocation.Y;
                }
                // bottom clip
                if (ydiff > 0 && upMovement)
                {
                    newDiagramLocation.Y = oldDiagramLocation.Y;
                }
                
                //Reset position when zoomed back in
                if (wasZoomedOut)
                {
                    newDiagramLocation = new Point(0, 0);
                    wasZoomedOut = false;
                }
            }
            //if (newScale <= 1) {
            else {
                newViewportCentre = viewportCentre;
                oldViewportCentre = newViewportCentre;

                oldDiagramLocation = new Point(diagramLeft, diagramTop);
                //if (diagramLeft != 0 || diagramTop != 0) {
                //    //shiftAmount = new Point(-newDiagramLocation.X, -newDiagramLocation.Y);
                //    newDiagramLocation = new Point(0, 0);
                //}
                wasZoomedOut = true;

            }

            if (isDragging) {
                    diagram.SetValue(Canvas.TopProperty, newDiagramLocation.Y);
                    diagramReflection.SetValue(Canvas.TopProperty, newDiagramLocation.Y);
                    diagram.SetValue(Canvas.LeftProperty, newDiagramLocation.X);
                    diagramReflection.SetValue(Canvas.LeftProperty, newDiagramLocation.X);

                oldDiagramLocation = newDiagramLocation;
                renderbounds = new Rect(0, 0, diagram.RenderSize.Width, diagram.RenderSize.Height);
                diagrambounds = new Rect(offsetsrc.X, offsetsrc.Y, diagramWidth, diagramHeight);
                return;
            }

            


            //newViewportCentre = viewportCentre;
            //oldViewportCentre = viewportCentre;
            animateDiagram(diagram);
            if (diagramReflection != null)
                animateDiagram(diagramReflection);

            // set the old scale and center to the new values
            oldScale = newScale;
            oldViewportCentre = newViewportCentre;
            oldDiagramLocation = newDiagramLocation;
        }


        private void animateDiagram(FrameworkElement diagramToAnimate) {
            Storyboard sb = new Storyboard();
            Duration duration = new Duration(animTime);

            // scale X
            DoubleAnimation daScaleX = new DoubleAnimation();
            sb.Children.Add(daScaleX);
            daScaleX.From = oldScale;
            daScaleX.To = newScale;
            daScaleX.Duration = duration;
            Storyboard.SetTarget(daScaleX, diagramToAnimate);
            Storyboard.SetTargetProperty(daScaleX, getScalePath(diagramToAnimate, typeof(ScaleTransform), "ScaleX"));

            // scale Y
            DoubleAnimation daScaleY = new DoubleAnimation();
            sb.Children.Add(daScaleY);
            daScaleY.From = oldScale;
            daScaleY.To = newScale;
            daScaleY.Duration = duration;
            Storyboard.SetTarget(daScaleY, diagramToAnimate);
            Storyboard.SetTargetProperty(daScaleY, getScalePath(diagramToAnimate, typeof(ScaleTransform), "ScaleY"));

            // centre X
            DoubleAnimation daCentreX = new DoubleAnimation();
            sb.Children.Add(daCentreX);
            daCentreX.From = oldViewportCentre.X;
            daCentreX.To = newViewportCentre.X;
            daCentreX.Duration = duration;
            Storyboard.SetTarget(daCentreX, diagramToAnimate);
            Storyboard.SetTargetProperty(daCentreX, getScalePath(diagramToAnimate, typeof(ScaleTransform), "CenterX"));

            // centre Y
            DoubleAnimation daCentreY = new DoubleAnimation();
            sb.Children.Add(daCentreY);
            daCentreY.From = oldViewportCentre.Y;
            daCentreY.To = newViewportCentre.Y;
            daCentreY.Duration = duration;
            Storyboard.SetTarget(daCentreY, diagramToAnimate);
            Storyboard.SetTargetProperty(daCentreY, getScalePath(diagramToAnimate, typeof(ScaleTransform), "CenterY"));

            // location X
            DoubleAnimation daLocationX = new DoubleAnimation();
            sb.Children.Add(daLocationX);
            daLocationX.From = oldDiagramLocation.X;
            daLocationX.To = newDiagramLocation.X;
            daLocationX.Duration = duration;
            Storyboard.SetTarget(daLocationX, diagramToAnimate);
            Storyboard.SetTargetProperty(daLocationX, new PropertyPath(Canvas.LeftProperty));

            // location Y
            DoubleAnimation daLocationY = new DoubleAnimation();
            sb.Children.Add(daLocationY);
            daLocationY.From = oldDiagramLocation.Y;
            daLocationY.To = newDiagramLocation.Y;
            daLocationY.Duration = duration;
            Storyboard.SetTarget(daLocationY, diagramToAnimate);
            Storyboard.SetTargetProperty(daLocationY, new PropertyPath(Canvas.TopProperty));



            //begin the animation
            sb.Begin();

        }

        /// <summary>
        /// Returns the propert path for a UIElement property part of a TransformGroup (includes ScaleTransform, TranslateTransform etc)
        /// </summary>
        /// <param name="e">The element to get the path from</param>
        /// <param name="type">The type of the property's parent</param>
        /// <param name="property">The final property</param>
        /// <returns></returns>
        private PropertyPath getScalePath(UIElement e, Type type, string property)
        {
            TransformGroup tg = e.RenderTransform as TransformGroup;
            if (tg == null)
                return null;
            for (int i = 0; i < tg.Children.Count; i++) {
                if (tg.Children[i].GetType() == type)
                    return new PropertyPath(string.Format("(UIElement.RenderTransform).(TransformGroup.Children)[{0}].({1}.{2})", i,type.Name,property));
            }
            
            return null;
        }


        /// <summary>
        /// Return difference between two points. Always returns a positive integer;
        /// </summary>
        /// <param name="a">First point</param>
        /// <param name="b">Second point</param>
        /// <returns></returns>
        private static double distanceBetweenPoints(Point a, Point b) {
            double xdiff = a.X - b.X;
            double ydiff = a.Y - b.Y;
            double diff = Math.Sqrt(xdiff * xdiff + ydiff * ydiff);
            if (diff > 0)
                return diff;
            else
                return -diff;
        }

        /// <summary>
        /// Check to see if transformation nodes exist in the object's xaml, if not, create
        /// </summary>
        private void loadObjects(FrameworkElement dg) {
            if (dg == null)
                return;
            tg = dg.RenderTransform as TransformGroup;
            if (tg == null)
                tg = new TransformGroup();

            foreach (object o in tg.Children) {
                if (o.GetType() == typeof(ScaleTransform))
                    scale = o as ScaleTransform;

                if (o.GetType() == typeof(TranslateTransform))
                    trans = o as TranslateTransform;
            }
            if (tg.Children.Count < 2)
            {
                tg.Children.Add(new TranslateTransform());
                tg.Children.Add(new ScaleTransform());
            }

           dg.RenderTransform = tg;

        }

    }
}
